package gti310.tp2.audio;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;

import gti310.tp2.io.*;

public class ConcreteAudioFilter implements AudioFilter {

	public static final int SUPPORTED_SAMPLING = 44100;
	public static final int OUTPUT_SAMPLING = 8000;

	private ByteArrayOutputStream rHeader = new ByteArrayOutputStream();
	private ByteArrayOutputStream rData = new ByteArrayOutputStream();

	private int channel;
	private int BitsPerSample;
	private short BlockAlign;

	private FileSource in;
	private FileSink out;

	// Constructor
	public ConcreteAudioFilter(FileSource in, FileSink out) throws Exception {
		this.in = in;
		this.out = out;
	}

	private void Filter() throws Exception {
		byte[] buffer;
		ByteBuffer byteBuff;

		/**
		 * RIFF HEADER
		 */

		// ChunkID
		//O(1)
		buffer = in.pop(4);
		if (!new String(buffer).equals("RIFF"))
			throw new Exception("Invalid file: Not a RIFF container");
		this.rHeader.write(buffer);

		// ChunkSize (should be ideally recalculated at the end of sampling)
		//O(1)
		buffer = in.pop(4);
		this.rHeader.write(buffer);

		// Format
		//O(1)
		buffer = in.pop(4);
		if (!new String(buffer).equals("WAVE"))
			throw new Exception("Invalid file: Not a WAVE container");
		this.rHeader.write(buffer);

		/**
		 * END OF RIFF HEADER
		 */

		/**
		 * WAVE HEADER fmt subchunk
		 */

		// Subchunk1ID
		//O(1)
		buffer = in.pop(4);
		if (!new String(buffer).equals("fmt "))
			throw new Exception("Invalid format: fmt subchunk invalid");
		this.rHeader.write(buffer);

		// Subchunk1Size
		//O(1)
		buffer = in.pop(4);
		this.rHeader.write(buffer);

		// AudioFormat
		//O(1)
		buffer = in.pop(2);
		if (!(buffer[0] == 1))
			throw new Exception("Invalid format: Compression not supported");
		this.rHeader.write(buffer);

		// NumChannels
		//O(1)
		buffer = in.pop(2);
		if (buffer[0] > 2)
			throw new Exception("Invalid format: Channel number not supported");
		this.channel = (int) buffer[0];
		this.rHeader.write(buffer);

		// SampleRate
		//O(1)
		buffer = in.pop(4);
		byteBuff = ByteBuffer.wrap(buffer);
		byteBuff.order(ByteOrder.LITTLE_ENDIAN);
		if (byteBuff.getInt() != SUPPORTED_SAMPLING)
			throw new Exception("Invalid format: Sampling rate not supported");

		//O(1)
		byteBuff = ByteBuffer.wrap(new byte[4]);
		byteBuff.order(ByteOrder.LITTLE_ENDIAN);
		byteBuff.putInt(OUTPUT_SAMPLING);
		this.rHeader.write(byteBuff.array()); // Add new sample rate to header

		// ByteRate
		//O(1)
		buffer = in.pop(4);
		byteBuff = ByteBuffer.wrap(buffer);
		byteBuff.order(ByteOrder.LITTLE_ENDIAN);
		this.BitsPerSample = (8 * byteBuff.getInt())
				/ (SUPPORTED_SAMPLING * this.channel);
		if (!(this.BitsPerSample == 8 || this.BitsPerSample == 16))
			throw new Exception("Invalid format: Bits per sample not supported");

		//O(1)
		byteBuff = ByteBuffer.wrap(new byte[4]);
		byteBuff.order(ByteOrder.LITTLE_ENDIAN);
		byteBuff.putInt(OUTPUT_SAMPLING * this.channel * this.BitsPerSample / 8);
		this.rHeader.write(byteBuff.array()); // Add new byte rate to header

		// BlockAlign
		//O(1)
		buffer = in.pop(2);
		byteBuff = ByteBuffer.wrap(buffer);
		byteBuff.order(ByteOrder.LITTLE_ENDIAN);
		this.BlockAlign = byteBuff.getShort();
		if ((this.channel * this.BitsPerSample / 8) != this.BlockAlign)
			throw new Exception("Invalid format: Block Align mismatch");
		this.rHeader.write(buffer);

		// BitsPerSample
		//O(1)
		buffer = in.pop(2);
		byteBuff = ByteBuffer.wrap(buffer);
		byteBuff.order(ByteOrder.LITTLE_ENDIAN);
		int BitsPerSample = byteBuff.getShort();
		if (this.BitsPerSample != BitsPerSample)
			throw new Exception("Invalid format: BitsPerSample mismatch");
		this.rHeader.write(buffer);

		/**
		 * END OF WAVE HEADER fmt subchunk
		 */

		/**
		 * WAVE HEADER Data subchunk
		 */

		// Subchunk2ID
		// note: Because of the extra parameters in the fmt subchunk
		// we are not guaranteed to catch the "data" value on the
		// next pop. This is why we use a do-while.
		//O(N)
		do {
			buffer = in.pop(4);
		} while (!new String(buffer).equals("data"));
		this.rHeader.write(buffer);

		// Subchunk2Size
		// note: This param must reflect the size of the data
		// chunk. This value is known after resampling.
		//O(N)
		buffer = in.pop(4);
		reSample();
		byteBuff = ByteBuffer.wrap(new byte[4]);
		byteBuff.order(ByteOrder.LITTLE_ENDIAN);
		byteBuff.putInt(this.rData.size());
		this.rHeader.write(byteBuff.array());

		/**
		 * END OF WAVE HEADER Data subchunk
		 */

		// Adding the data part to the header
		this.rHeader.write(this.rData.toByteArray());

		// Writing it all to the file
		this.out.push(this.rHeader.toByteArray());
	}

	private void reSample() {
		double samplingFactor = ((float) SUPPORTED_SAMPLING / (float) OUTPUT_SAMPLING) - 1;
		double nextStep = samplingFactor;
		double nextStepDecimal = nextStep - (int) nextStep;

		int leftValue;
		int rightValue;
		byte[] sample = new byte[this.BlockAlign];
		byte[] leftSample = new byte[this.BlockAlign];
		byte[] rightSample = new byte[this.BlockAlign];
		ByteBuffer byteBuffS;
		ByteBuffer byteBuffL;
		ByteBuffer byteBuffR;

		// we must hit 9 times a zero filled byte array before breaking the
		// loop.
		// Because FileSource doesn't seems to return null when reaching
		// the EOF. Instead it returns a byte array full of zeros.
		// 
		// Not a very beautiful solution
		boolean EOF = false;
		int hit = 0;
		int threshold = 9;
		byte[] emptyArray = new byte[(this.BlockAlign * (int) nextStep)];

		// Sampling
		//O(N)
		do {
			// Popping useless data and checking for EOF
			//O(1)
			if (Arrays.equals(in.pop((this.BlockAlign * (int) (nextStep))),
					emptyArray)) {
				hit++;
				if (hit == threshold) {
					EOF = true;
				}
			}
			//Get the next two block for interpolation
			//O(1)
			leftSample = in.pop(this.BlockAlign);
			rightSample = in.pop(this.BlockAlign);

			// Interpolation
			// Me gusta ByteBuffers
			byteBuffL = ByteBuffer.wrap(leftSample);
			byteBuffL.order(ByteOrder.LITTLE_ENDIAN);
			byteBuffR = ByteBuffer.wrap(rightSample);
			byteBuffR.order(ByteOrder.LITTLE_ENDIAN);
			byteBuffS = ByteBuffer.wrap(sample);
			byteBuffS.order(ByteOrder.LITTLE_ENDIAN);
			
			//Black Magic
			//O(N)
			for (int i = 0; i < this.channel; i++) {
				leftValue = 0;
				rightValue = 0;

				//O(1)
				if (this.BitsPerSample == 16) {
					leftValue = byteBuffL.getShort();
					rightValue = byteBuffR.getShort();
					byteBuffS
							.putShort( (short) (leftValue + (rightValue - leftValue)
									* nextStepDecimal));
				//O(1)
				} else {
					leftValue = (byteBuffL.get() & 0xFF);
					rightValue = (byteBuffR.get() & 0xFF);
					byteBuffS.put((byte) (leftValue + (rightValue - leftValue)
							* nextStepDecimal));
				}
			}

			try {
				// Add data to stream
				this.rData.write(byteBuffS.array());
			} catch (IOException e) {
				e.printStackTrace();
			}

			nextStep = samplingFactor - (1 - nextStepDecimal);
			nextStepDecimal = nextStep - (int) nextStep;
		} while (EOF == false);
	}

	@Override
	public void process() {
		try {
			Filter();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}

